<?php

function transamp($template) {
    $template = str_replace('&', '&amp;', $template);
    $template = str_replace('&amp;amp;', '&amp;', $template);
    $template = str_replace('\"', '"', $template);
    return $template;
}

function stripvtags($expr, $statement) {
    $expr = str_replace("\\\"", "\"", preg_replace("/\<\?\=(\\\$.+?)\?\>/s", "\\1", $expr));
    $statement = str_replace("\\\"", "\"", $statement);
    return $expr . $statement;
}

function addquote($var) {
    return str_replace("\\\"", "\"", preg_replace("/\[([a-zA-Z0-9_\-\.\x7f-\xff]+)\]/s", "['\\1']", $var));
}

function stripscriptamp($s) {
    $s = str_replace('&amp;', '&', $s);
    return "<script src=\"$s\" type=\"text/javascript\"></script>";
}

function stripblock($var, $s) {
    $s = str_replace('\\"', '"', $s);
    $s = preg_replace("/<\?=\\\$(.+?)\?>/", "{\$\\1}", $s);
    preg_match_all("/<\?=(.+?)\?>/e", $s, $constary);
    $constadd = '';
    $constary[1] = array_unique($constary[1]);
    foreach ($constary[1] as $const) {
        $constadd .= '$__' . $const . ' = ' . $const . ';';
    }
    $s = preg_replace("/<\?=(.+?)\?>/", "{\$__\\1}", $s);
    $s = str_replace('?>', "\n\$$var .= <<<EOF\n", $s);
    $s = str_replace('<?', "\nEOF;\n", $s);
    return "<?\n$constadd\$$var = <<<EOF\n" . $s . "\nEOF;\n?>";
}

class Template {

    const DIR_SEP = DIRECTORY_SEPARATOR;

    //模板实例
    private static $_instance = NULL;
    //模板参数信息
    protected $_options = array();

    //单件模式调用方法
    public static function getInstance() {
        //if (!self::$_instance instanceof self){
        if (self::$_instance == NULL) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }

    //构造方法 关闭前门
    private function __construct() {
        
    }

    //设定模板参数信息
    public function setOptions($tpl_dir, $cache_dir, $auto_update, $cache_lt, $suffix) {
        $options = array(
            'template_dir' => $tpl_dir,
            'cache_dir' => $cache_dir,
            'auto_update' => $auto_update,
            'cache_lifetime' => $cache_lt,
            'suffix' => $suffix,
        );

        foreach ($options as $name => $value) {
            $this->set($name, $value);
        }
    }

    //设定模板参数
    public function set($name, $value) {
        switch ($name) {
            case 'template_dir':
                $value = $this->_trimpath($value);
                if (!file_exists($value))
                    $this->_throwException("未找到指定的模板目录 \"$value\"");
                $this->_options['template_dir'] = $value;
                break;
            case 'cache_dir':
                $value = $this->_trimpath($value);
                if (!file_exists($value))
                    $this->_throwException("未找到指定的缓存目录 \"$value\"");
                $this->_options['cache_dir'] = $value;
                break;
            case 'auto_update':
                $this->_options['auto_update'] = (boolean) $value;
                break;
            case 'cache_lifetime':
                $this->_options['cache_lifetime'] = (float) $value;
                break;
            case 'suffix':
                $this->_options['suffix'] = $value;
                break;
            default:
                $this->_throwException("未知的模板配置选项 \"$name\"");
        }
    }

    /**
     * 通过魔术方法设定模板参数
     *
     * @see    Template::set()
     * @param  string $name  参数名称
     * @param  mixed  $value 参数值
     * @return void
     */
    public function __set($name, $value) {
        $this->set($name, $value);
    }

    //获取模板文件
    public function getfile($file) {
        $file = $file . $this->_options['suffix'];   //设置模板文件后缀
        $cachefile = $this->_getCacheFile($file);
        if (!file_exists($cachefile))
            $this->cache($file);
        return $cachefile;
    }

    /**
     * 检测模板文件是否需要更新缓存
     *
     * @param  string  $file    模板文件名称
     * @param  string  $md5data 模板文件 md5 校验信息
     * @param  integer $md5data 模板文件到期时间校验信息
     * @return void
     */
    public function check($file, $md5data, $expireTime) {
        if ($this->_options['auto_update']
                && md5_file($this->_getTplFile($file)) != $md5data)
            $this->cache($file);
        if ($this->_options['cache_lifetime'] != 0
                && (time() - $expireTime >= $this->_options['cache_lifetime'] * 60))
            $this->cache($file);
    }

    /**
     * 对模板文件进行缓存
     *
     * @param  string  $file    模板文件名称
     * @return void
     */
    public function cache($file) {
        $tplfile = $this->_getTplFile($file);

        if (!is_readable($tplfile)) {
            $this->_throwException("模板文件 \"$tplfile\" 未找到或者无法打开");
        }
        $template="";
        //取得模板内容
        $template = file_get_contents($tplfile);

        //过滤 <!--{}-->
        $template = preg_replace("/\<\!\-\-\{(.+?)\}\-\-\>/s", "{\\1}", $template);

        //替换语言包变量
        $template = preg_replace("/\{lang\s+(.+?)\}/ies", "lang('\\1')", $template);

        //替换 PHP 换行符
        $template = str_replace("{LF}", "<?=\"\\n\"?>", $template);

        //替换直接变量输出
        $varRegexp = "((\\\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)"
                . "(\[[a-zA-Z0-9_\-\.\"\'\[\]\$\x7f-\xff]+\])*)";
        $template = preg_replace("/\{(\\\$[a-zA-Z0-9_\[\]\'\"\$\.\x7f-\xff]+)\}/s", "<?=\\1?>", $template);
        $template = preg_replace("/$varRegexp/es", "addquote('<?=\\1?>')", $template);
        $template = preg_replace("/\<\?\=\<\?\=$varRegexp\?\>\?\>/es", "addquote('<?=\\1?>')", $template);

        //替换模板载入命令
        $template = preg_replace(
                "/[\n\r\t]*\{template\s+([a-z0-9_]+)\}[\n\r\t]*/is", "\r\n<? include(\$template->getfile('\\1')); ?>\r\n", $template
        );
        $template = preg_replace(
                "/[\n\r\t]*\{template\s+(.+?)\}[\n\r\t]*/is", "\r\n<? include(\$template->getfile(\\1)); ?>\r\n", $template
        );

        //替换特定函数
        $template = preg_replace(
                "/[\n\r\t]*\{eval\s+(.+?)\}[\n\r\t]*/ies", "stripvtags('<? \\1 ?>','')", $template
        );
        $template = preg_replace(
                "/[\n\r\t]*\{echo\s+(.+?)\}[\n\r\t]*/ies", "stripvtags('<? echo \\1; ?>','')", $template
        );
        $template = preg_replace(
                "/([\n\r\t]*)\{elseif\s+(.+?)\}([\n\r\t]*)/ies", "stripvtags('\\1<? } elseif(\\2) { ?>\\3','')", $template
        );
        $template = preg_replace(
                "/([\n\r\t]*)\{else\}([\n\r\t]*)/is", "\\1<? } else { ?>\\2", $template
        );

        //替换循环函数及条件判断语句
        $nest = 5;
        for ($i = 0; $i < $nest; $i++) {
            $template = preg_replace(
                    "/[\n\r\t]*\{loop\s+(\S+)\s+(\S+)\}[\n\r]*(.+?)[\n\r]*\{\/loop\}[\n\r\t]*/ies", "stripvtags('<? if(is_array(\\1)) { foreach(\\1 as \\2) { ?>','\\3<? } } ?>')", $template
            );
            $template = preg_replace(
                    "/[\n\r\t]*\{loop\s+(\S+)\s+(\S+)\s+(\S+)\}[\n\r\t]*(.+?)[\n\r\t]*\{\/loop\}[\n\r\t]*/ies", "stripvtags('<? if(is_array(\\1)) { foreach(\\1 as \\2 => \\3) { ?>','\\4<? } } ?>')", $template
            );
            $template = preg_replace(
                    "/([\n\r\t]*)\{if\s+(.+?)\}([\n\r]*)(.+?)([\n\r]*)\{\/if\}([\n\r\t]*)/ies", "stripvtags('\\1<? if(\\2) { ?>\\3','\\4\\5<? } ?>\\6')", $template
            );
        }

        //常量替换
        $template = preg_replace(
                "/\{([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\}/s", "<?=\\1?>", $template
        );

        //删除 PHP 代码断间多余的空格及换行
        $template = preg_replace("/ \?\>[\n\r]*\<\? /s", " ", $template);

        //其他替换
        $template = preg_replace(
                "/\"(http)?[\w\.\/:]+\?[^\"]+?&[^\"]+?\"/e", "transamp('\\0')", $template
        );
        $template = preg_replace(
                "/\<script[^\>]*?src=\"(.+?)\".*?\>\s*\<\/script\>/ise", "stripscriptamp('\\1')", $template
        );
        $template = preg_replace(
                "/[\n\r\t]*\{block\s+([a-zA-Z0-9_]+)\}(.+?)\{\/block\}/ies", "stripblock('\\1', '\\2')", $template
        );

        //添加 md5 及过期校验
        $md5data = md5_file($tplfile);
        $expireTime = time();
        $template = "<? if (!class_exists('Template')) die('Access Denied');"
                . "\$tpl->getInstance()->check('$file', '$md5data', $expireTime);"
                . "?>$template";

        //写入缓存文件
        $cachefile = $this->_getCacheFile($file);
        $makepath = $this->_makepath($cachefile);
        if ($makepath !== true)
            $this->_throwException("无法创建缓存目录 \"$makepath\"");
        file_put_contents($cachefile, $template);
    }

    /**
     * 将路径修正为适合操作系统的形式
     *
     * @param  string $path 路径名称
     * @return string
     */
    protected function _trimpath($path) {
        return str_replace(array('/', '\\', '//', '\\\\'), self::DIR_SEP, $path);
    }

    /**
     * 获取模板文件名及路径
     *
     * @param  string $file 模板文件名称
     * @return string
     */
    protected function _getTplFile($file) {
        return $this->_trimpath($this->_options['template_dir'] . self::DIR_SEP . $file);
    }

    /**
     * 获取模板缓存文件名及路径
     *
     * @param  string $file 模板文件名称
     * @return string
     */
    protected function _getCacheFile($file) {
        $file = preg_replace('/\.[a-z0-9\-_]+$/i', '.cache.php', $file);
        return $this->_trimpath($this->_options['cache_dir'] . self::DIR_SEP . $file);
    }

    /**
     * 根据指定的路径创建不存在的文件夹
     *
     * @param  string  $path 路径/文件夹名称
     * @return string
     */
    protected function _makepath($path) {
        $dirs = explode(self::DIR_SEP, dirname($this->_trimpath($path)));
        $tmp = '';
        foreach ($dirs as $dir) {
            $tmp .= $dir . self::DIR_SEP;
            if (!file_exists($tmp) && !@mkdir($tmp, 0777))
                return $tmp;
        }
        return true;
    }

    /**
     * 抛出一个错误信息
     *
     * @param string $message
     * @return void
     */
    protected function _throwException($message) {
        throw new Exception($message);
    }
}
